/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ThumbsDown.
 *
 * The Initial Developer of the Original Code is Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2012
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


function LinkXpathPage() // extend WizardPage
{
    var depends = "selected";
    WizardPage.call(this, depends);
}

LinkXpathPage.prototype =
{
    get linkUrls()
    {
        if (! this._linkUrls)
            this._linkUrls = [this._resolveLinkUrl(thumbnail) for each (thumbnail in model.selected)];
        
        return this._linkUrls;
    },
    
    get selectedThumbnail()
    {
        return model.selected[this.$linkList.selectedIndex];
    },
    
    onPageShow: function()
    {
        if (this.firstRun)
        {
            this.$deck = document.getElementById("deck_linkXpath");
            this.$elementTree = document.getElementById("tree_linkXpath_elem");
            this.$attributeTree = document.getElementById("tree_linkXpath_attr");
            this.$xpath = document.getElementById("tbox_linkXpath");
            this.$linkList = document.getElementById("rlbox_linkXpath");
            
            this.$elementTree.view = this.elementTreeView = new NodeTreeView(new DomElementTreeNode(model.doc.documentElement));
            this.$attributeTree.view = this.attributeTreeView = new ElementTreeView();
        }
        
        if (this.dirty.page)
        {
            if (this.elementTreeView.thumbnailNode)
                this.elementTreeView.thumbnailNode.node.thumbnail = null;
            
            this.elementTreeView.selection.clearSelection();
            this.attributeTreeView.model = null;
            this.$linkList.selectedIndex = -1;
            
            this._updateThumbnailLinks();
        }
    },
    
    onThumbnailSelect: function()
    {
        let thumbnail = this.selectedThumbnail;
        if (! thumbnail)
            return;
        
        let nodeRow = this._getNodeTreeRow(this.elementTreeView, thumbnail.element);
        if (nodeRow)
        {
            if (this.elementTreeView.thumbnailNode)
                this.elementTreeView.thumbnailNode.node.thumbnail = null;
            
            this.elementTreeView.selection.clearSelection();
            
            let {node, row} = nodeRow;
            this.elementTreeView.thumbnailNode = node;
            this.elementTreeView.thumbnailNode.node.thumbnail = thumbnail;
            this.elementTreeView.treeBoxObject.ensureRowIsVisible(row);
            this.elementTreeView.treeBoxObject.invalidate();
        }
    },
    
    // re-ensure row is visible when item is clicked and item is already selected (select is not fired)
    onThumbnailClick: function(e)
    {
        if (e.button != 0)
            return;
        
        let rlitem = e.target;
        while (rlitem && (rlitem.nodeName != "richlistitem"))
            rlitem = rlitem.parentNode;
        
        if (! rlitem)
            return;
        
        let thumbnail = this.selectedThumbnail;
        if (! thumbnail)
            return;
        
        let nodeRow = this._getNodeTreeRow(this.elementTreeView, thumbnail.element);
        if (nodeRow)
            this.elementTreeView.treeBoxObject.ensureRowIsVisible(nodeRow.row);
        
        this.toggleHelp(false);
    },
    
    onElementTreeSelect: function()
    {
        let row = this.elementTreeView.selectedIndex;
        let model = null;
        if (row != -1)
            model = this.elementTreeView.get(row).node.element;
        
        this.attributeTreeView.model = model;
    },
    
    onAttributeTreeSelect: function()
    {
        let index = this.attributeTreeView.selectedIndex;
        if (index == -1)
            return;
        
        let thumbnailElement = this.selectedThumbnail.element;
        let linkElement = this.attributeTreeView.model;
        let linkAttribute = this.attributeTreeView._model[index];
        
        if (! (thumbnailElement && linkElement))
            return;
        
        let xpath = this._generateXpath(thumbnailElement, linkElement) + "/@" + linkAttribute.name;
        if (xpath.substr(0, 2) == "./")
            xpath = xpath.substr(2);
        
        this.$xpath.value = xpath;
        
        this._updateThumbnailLinks();
        wizard.updateWizardButtons();
    },
    
    onXpathInput: function()
    {
        this._updateThumbnailLinks();
        wizard.updateWizardButtons();
    },
    
    _getNodeTreeRow: function(nodeTreeView, $element)
    {
        if (! $element)
            return null;
        
        if (nodeTreeView.root.node.element == $element)
            return {node: nodeTreeView.root, row: 0};
        
        let parent = this._getNodeTreeRow(nodeTreeView, $element.parentNode);
        if (! parent)
            return null;
        
        if (! parent.node.open)
            nodeTreeView.toggleOpenState(parent.row);
        
        let count = 0;
        for each (let node in parent.node.children)
            if (node.node.element == $element)
                return {node: node, row: parent.row + node.getParentIndex()};
        
        throw new Error("unreachable");
    },
    
    _resolveLinkUrl: function(thumbnail)
    {
        try
        {
            let xpath = this.$xpath.value.trim();
            if (xpath)
            {
                let spec = this._evalXpath(xpath, thumbnail.element).trim();
                if (spec)
                    return Services.io.newURI(spec, null, model.doc.documentURIObject).spec;
            }
        }
        catch (e) { /* ignore */ }
        
        return null;
    },
    
    _evalXpath: function(expr, $context)
    {
        try
        {
            let result = model.doc.evaluate(expr, $context, null, XPathResult.STRING_TYPE, null);
            return result.stringValue;
        }
        catch (e)
        {
            return null;
        }
    },
    
    _generateXpath: function(e1, e2)
    {
        if (e1 == e2)
            return ".";
        
        let xpath = "";
        let parent = getCommonParent(e1, e2);
        if (! parent)
            throw new Error("no common parent");
        
        // e2 -> parent (xpath: parent -> e2)
        for (let element = e2; element != parent; element = element.parentNode)
        {
            let index = 1;
            for (let sibling = element.previousElementSibling; sibling; sibling = sibling.previousElementSibling)
                if (sibling.nodeName == element.nodeName)
                    index++;
            
            xpath = element.nodeName.toLowerCase() + "[" + index + "]/" + xpath;
        }
        
        // e1 -> parent (xpath: e1 -> parent -> 2)
        for (let element = e1; element != parent; element = element.parentNode)
            xpath = "../" + xpath;
        
        // strip trailing '/'
        xpath = xpath.substr(0, xpath.length - 1);
        
        return xpath;
    },
    
    _updateThumbnailLinks: function()
    {
        let index = this.$linkList.selectedIndex;
        
        clearListbox(this.$linkList);
        
        this._linkUrls = null;
        
        for each (let linkUrl in this.linkUrls)
        {
            let $rlitem = this.$linkList.appendItem(linkUrl || "<error>");
            $rlitem.firstChild.crop = "none";
        }
        
        this._selectLink(index);
    },
    
    _selectLink: function(index)
    {
        if (! ((0 <= index) && (index < this.$linkList.itemCount)))
            return;
        
        this.$linkList.suppressOnSelect = true;
        this.$linkList.selectItem(this.$linkList.getItemAtIndex(index));
        this.$linkList.ensureIndexIsVisible(index);
        this.$linkList.suppressOnSelect = false;
    },
    
    canAdvance: function()
    {
        if (this.isHelpOpen)
            return false;
        
        if (! this.$xpath.value.trim())
            return false;
        
        if (! this.linkUrls.every(function(url) { return (url && (url.substr(0, 4) == "http")); }))
            return false;
        
        return true;
    },
    
    onPageAdvanced: function()
    {
        try
        {
            // validate linkUrls (copied from linktype)
            scriptWizard.getUrlTokens(this.linkUrls);
        }
        catch (e if (typeof(e) == "string"))
        {
            model.error = e;
            return wizard.advance("error");
        }
        
        model.linkExpr = this.$xpath.value.trim();
        model.linkUrls = this.linkUrls;
        
        return wizard.advance("target");
    },
}

extend(LinkXpathPage, WizardPage);

pages["linkXpath"] = new LinkXpathPage();


function DomElementTreeNode(element, parent)
{
    this.parent = parent;
    this.element = element;
    this.text = element.nodeName.toLowerCase();
    this.separator = false;
    
    this.thumbnail = null;
}

DomElementTreeNode.prototype =
{
    get hasChildren() { return this.element.firstElementChild; },
    
    getChildren: function()
    {
        let children = [];
        
        let child = this.element.firstElementChild;
        while (child)
        {
            children.push(new DomElementTreeNode(child, this));
            child = child.nextElementSibling;
        }
        
        return children;
    },
    
    getText: function(col)
    {
        return this.text;
    },
    
    getCellProperties: function(col)
    {
        if (this.thumbnail)
            return "thumbnail thumbnail_" + this.thumbnail.type;
        else
            return "non-thumbnail";
    },
}


function InternalTreeNode(parent, node, level)
{
    this.parent = parent;
    this.node = node;
    this.level = level;
    this.open = false;
    this.size = 1;
}

InternalTreeNode.prototype =
{
    get children()
    {
        if (! this._children)
            this._children = [new InternalTreeNode(this, child, this.level + 1) for each (child in this.node.getChildren())];
        
        return this._children;
    },
    
    get: function(index)
    {
        if (index == 0)
            return this;
        
        index -= 1;
        for each (let child in this.children)
        {
            if (index < child.size)
                return child.get(index);
            else
                index -= child.size;
        }
        
        throw Cr.NS_ERROR_UNEXPECTED;
    },
    
    getParentIndex: function()
    {
        if (! this.parent)
            return -1;
        
        let count = 1;
        for each (let sibling in this.parent.children)
            if (sibling != this)
                count += sibling.size;
            else
                return count;
        
        throw Cr.NS_ERROR_UNEXPECTED;
    },
    
    toggleOpenState: function()
    {
        if (this.open)
        {
            let childCount = this.size - 1;
            
            this.size = 1;
            this.open = false;
            
            let parent = this.parent;
            while (parent)
            {
                parent.size -= childCount;
                parent = parent.parent;
            }
            
            return -childCount;
        }
        else
        {
            let childCount = 0;
            for each (let child in this.children)
                childCount += child.size;
            
            this.size = childCount + 1;
            this.open = true;
            
            let parent = this.parent;
            while (parent)
            {
                parent.size += childCount;
                parent = parent.parent;
            }
            
            return childCount;
        }
    },
}


function NodeTreeView(rootNode)
{
    this.root = new InternalTreeNode(null, rootNode, 0);
    this._node = null;
    this._nodeIndex = -1;
}

NodeTreeView.prototype =
{
    QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
    
    // nsITreeView
    
    get rowCount() { return this.root.size; },
    
    setTree: function(treeBoxObject) { this.treeBoxObject = treeBoxObject; },
    
    isContainer: function(row) { return this.get(row).node.hasChildren; },
    isContainerEmpty: function(row) { return (! this.get(row).node.hasChildren); },
    isContainerOpen: function(row) { return this.get(row).open; },
    isSeparator: function(row) { return this.get(row).node.separator; },
    isSorted: function() { return false; },
    getLevel: function(row) { return this.get(row).level; },
    
    getParentIndex: function(row)
    {
        let count = this.get(row).getParentIndex();
        return ((count > 0) ? (row - count) : -1);
    },
    
    toggleOpenState: function(row)
    {
        let count = this.get(row).toggleOpenState();
        this.treeBoxObject.rowCountChanged(row + 1, count);
        this._node = null;
        this._nodeIndex = -1;
    },
    
    getCellText: function(row, col) { return this.get(row).node.getText(col); },
    getRowProperties: function(row) { return ""; },
    getColumnProperties: function(colid, col) { return ""; },
    getCellProperties: function(row, col) { return this.get(row).node.getCellProperties(col); },
    getImageSrc: function(row, col) { return null; },
    
    // internal
    
    get selectedIndex()
    {
        // selection.currentIndex is the currently focused index, but it's not neccessarily selected
        return ((this.selection.count == 1) ? SelectionIterator(this.selection).next() : -1);
    },
    
    get: function(row)
    {
        if (this._nodeIndex != row)
        {
            this._node = this.root.get(row);
            this._nodeIndex = row;
        }
        
        return this._node;
    },
}


function ElementTreeView() // extend TreeView
{
    this.model = null;
}

ElementTreeView.prototype =
{
    QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
    
    get model() { return this._model0; },
    set model(element)
    {
        if (this.treeBoxObject)
        {
            this.selection.selectEventsSuppressed = true;
            this.treeBoxObject.beginUpdateBatch();
            this.treeBoxObject.rowCountChanged(0, -this.rowCount)
        }
        
        this._model0 = element;
        this._model = [];
        
        if (this._model0)
        {
            let attrs = this._model0.attributes;
            for (let i = 0; i < attrs.length; i++)
            {
                let attr = attrs[i];
                this._model.push({name: attr.name.toLowerCase(), value: attr.value});
            }
        }
        
        this._model.sort(function(a,b) { return a.name.localeCompare(b.name); });
        
        if (this.treeBoxObject)
        {
            this.treeBoxObject.endUpdateBatch();
            this.selection.selectEventsSuppressed = false;
        }
    },
    
    get selectedIndex()
    {
        // selection.currentIndex is the currently focused index, but it's not neccessarily selected
        return ((this.selection.count == 1) ? SelectionIterator(this.selection).next() : -1);
    },
    
    // nsITreeView
    get rowCount() { return this._model.length; },
    
    getCellText: function(row, col)
    {
        let attr = this._model[row];
        
        switch (col.id)
        {
            case "tcol_linkXpath_name":  return attr.name;
            case "tcol_linkXpath_value": return attr.value;
            default:                     return "";
        } 
    },
    
    setTree: function(treeBoxObject) { this.treeBoxObject = treeBoxObject; },
    isContainer: function(row) { return false; },
    isSeparator: function(row) { return false; },
    isSorted: function() { return false; },
    getLevel: function(row) { return 0; },
    getImageSrc: function(row, col) { return null; },
    getRowProperties: function(row) { return ""; },
    getCellProperties: function(row, col) { return ""; },
    getColumnProperties: function(colid, col) { return ""; },
}
